/*
 * ﻿Copyright (C) 2013-2014 NewMain Softech
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
 * either express or implied. See the License for the specific language
 * governing permissions and limitations under the License.
 */
package com.newmainsoftech.eatestutil.container;

import java.text.MessageFormat;
import java.util.Date;
import java.util.HashSet;
import java.util.Set;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;

import javax.ejb.embeddable.EJBContainer;
import javax.naming.Context;
import javax.naming.NamingException;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Handler to close EJB context.<br />
 * 
 * @author Arata Y.
 * @see EjbContextCloseLockAspect
 */
public class EjbContextCloseHandlerImpl implements EjbContextCloseHandler {
	private Logger logger = LoggerFactory.getLogger( this.getClass());
		protected Logger getLogger() {
			return logger;
		}
		protected void setLogger( final Logger logger) {
			this.logger = logger;
		}
		
	private final ReentrantLock lockForClose = new ReentrantLock();
		/**
		 * Getter of lock being used for synchronizing operation in closing EJB context.<br />
		 * 
		 * @return Lock object being used for synchronizing operation in closing EJB context.
		 * @see EjbContextCloseLockAspect
		 */
		protected ReentrantLock getLockForClose() {
			return lockForClose;
		}
		public final static long CloseLockAcquireTimeOutDefaultvValue = 250L;
			public static long getCloseLockAcquireTimeOutDefaultvValue() {
				return CloseLockAcquireTimeOutDefaultvValue;
			}
		private long closeLockAcquireTimeOut 
		= EjbContextCloseHandlerImpl.getCloseLockAcquireTimeOutDefaultvValue();
			/**
			 * Getter of duration in millisecond until giving up to occupy lock for synchronizing  
			 * operation in closing EJB context.<br />
			 * Default to {@value #CloseLockAcquireTimeOutDefaultvValue}[ms].<br />
			 * 
			 * @return Duration in millisecond until giving up to occupy lock for synchronizing  
			 * operation in closing EJB context.
			 * @see EjbContextCloseLockAspect
			 */
			protected long getCloseLockAcquireTimeOut() {
				return closeLockAcquireTimeOut;
			}
			/**
			 * Setter of duration in millisecond until giving up occupying lock for synchronizing 
			 * operation in closing EJB context.<br />
			 * 
			 * @param closeLockAcquireTimeOut : Duration in millisecond until giving up to occupy 
			 * lock for synchronizing operation in closing EJB context.
			 * @see EjbContextCloseLockAspect
			 */
			protected void setCloseLockAcquireTimeOut( long closeLockAcquireTimeOut) {
				getCloseTask().setCloseLockAcquireTimeOut( closeLockAcquireTimeOut);
				this.closeLockAcquireTimeOut = closeLockAcquireTimeOut;
			}
		
	public static final boolean IsClosedByNonConsumerDefaultValue = true;
		public static boolean getIsClosedByNonConsumerDefaultValue() {
			return IsClosedByNonConsumerDefaultValue;
		}
	private boolean isClosedByNonConsumer = EjbContextCloseHandlerImpl.getIsClosedByNonConsumerDefaultValue();
		/**
		 * {@inheritDoc}<br />
		 * Default to {@value #IsClosedByNonConsumerDefaultValue}.
		 */
		@Override
		public boolean isClosedByNonConsumer() {
			return isClosedByNonConsumer;
		}
		/**
		 * {@inheritDoc}
		 */
		@Override
		public void setClosedByNonConsumer( boolean isClosedByNonConsumer) {
			this.isClosedByNonConsumer = isClosedByNonConsumer;
		}
		
	public static final long CloseDelayDefaultValue = 1500L;
		public static long getCloseDelayDefaultValue() {
			return CloseDelayDefaultValue;
		}
	private long closeDelay = EjbContextCloseHandlerImpl.getCloseDelayDefaultValue();
		/**
		 * {@inheritDoc}<br />
		 * Default to {@value #CloseDelayDefaultValue}[ms].
		 */
		@Override
		public long getCloseDelay() {
			return closeDelay;
		}
		/**
		 * {@inheritDoc}
		 */
		@Override
		public void setCloseDelay( long closeDelay) {
			this.closeDelay = closeDelay;
		}
	/**
	 * {@inheritDoc}
	 */
	@Override
	public boolean isContainerClosedToo() {
		return getCloseTask().isContainerClosedToo();
	}
	/**
	 * {@inheritDoc}
	 */
	@Override
	public void setContainerClosedToo( boolean isContainerClosedToo) {
		getCloseTask().setContainerClosedToo( isContainerClosedToo);
	}
			
	private Set<Object> consumers = new HashSet<Object>();
		protected Set<Object> getConsumers() {
			return consumers;
		}
		/**
		 * {@inheritDoc}
		 */
		@Override
		public void registerConsumer( final Object consumer) {
			if ( consumer == null) {
				throw new IllegalArgumentException(
						String.format(
								"Value of %1$s argument cannot be null.",
								"comsumer")
						);
			}
			
			if ( getCloseTask().hasBeenClosed()) {
				throw new IllegalStateException( "EJB context has already been closed.");
			}
			if ( getConsumers().add( consumer)) {
				cancelClose();
			}
		}
		/**
		 * {@inheritDoc}
		 */
		@Override
		public void removeConsumer( final Object consumer) {
			getConsumers().remove( consumer);
			
			if ( (getConsumerCount() < 1) && isClosedByNonConsumer()) {
				close();
			}
		}
		/**
		 * {@inheritDoc}
		 */
		@Override
		public int getConsumerCount() {
			return getConsumers().size();
		}
		
	/**
	 * Timer task being scheduled and close EJB context as actual worker for 
	 * <code>{@link EjbContextCloseHandlerImpl}</code>.
	 * 
	 * @author Arata Y.
	 * @see EjbContextCloseLockAspect
	 */
	public static class EjbContextCloseTask extends TimerTask {
		private Logger logger = LoggerFactory.getLogger( this.getClass());
			protected Logger getLogger() {
				return logger;
			}
			protected void setLogger( final Logger logger) {
				this.logger = logger;
			}

		private String ejbModuleName;
			/**
			 * Getter for name of either directory of or .jar file of EJB module.<br />
			 * 
			 * @return Name of either directory of or .jar file of EJB module. 
			 */
			public String getEjbModuleName() {
				return ejbModuleName;
			}
			/**
			 * Setter for name of either directory of or .jar file of EJB module.<br />
			 * 
			 * @param ejbModuleName : Name of either directory of or .jar file of EJB module.
			 */
			public void setEjbModuleName( final String ejbModuleName) {
				this.ejbModuleName = ejbModuleName;
			}
		private EJBContainer ejbContainer;
			/**
			 * Get instance of <code>EJBContainer</code>.<br />
			 * 
			 * @return Instance of <code>EJBContainer</code>.
			 */
			public EJBContainer getEjbContainer() {
				return ejbContainer;
			}
			/**
			 * Set instance of <code>EJBContainer</code>.<br />
			 * 
			 * @param ejbContainer : Instance of <code>EJBContainer</code>.
			 */
			public void setEjbContainer( final EJBContainer ejbContainer) {
				this.ejbContainer = ejbContainer;
			}
		private Context context;
			/**
			 * Get <code>Context</code> from <code>EJBContainer</code>.<br />
			 * 
			 * @return <code>Context</code> from <code>EJBContainer</code>.
			 */
			public Context getContext() {
				return context;
			}
			/**
			 * Set <code>Context</code> from <code>EJBContainer</code>.<br />
			 * 
			 * @param context : <code>Context</code> from <code>EJBContainer</code>.
			 */
			public void setContext( final Context context) {
				this.context = context;
			}

		private final ReentrantLock lockForClose;
			/**
			 * Getter of lock being used for synchronizing operation in closing EJB context.<br />
			 * 
			 * @return Lock object being used for synchronizing operation in closing EJB context.
			 * @see EjbContextCloseLockAspect
			 */
			protected ReentrantLock getLockForClose() {
				return lockForClose;
			}
		private long closeLockAcquireTimeOut; 
			/**
			 * Getter of duration in millisecond until giving up to occupy lock for synchronizing  
			 * operation in closing EJB context.<br />
			 * 
			 * @return Duration in millisecond until giving up to occupy lock for synchronizing  
			 * operation in closing EJB context.
			 * @see EjbContextCloseLockAspect
			 */
			protected long getCloseLockAcquireTimeOut() {
				return closeLockAcquireTimeOut;
			}
			/**
			 * Setter of duration in millisecond until giving up occupying lock for synchronizing 
			 * operation in closing EJB context.<br />
			 * 
			 * @param closeLockAcquireTimeOut : Duration in millisecond until giving up to occupy 
			 * lock for synchronizing operation in closing EJB context.
			 * @see EjbContextCloseLockAspect
			 */
			protected void setCloseLockAcquireTimeOut( long closeLockAcquireTimeOut) {
				this.closeLockAcquireTimeOut = closeLockAcquireTimeOut;
			}
		public static final boolean IsContainerClosedTooDefaultValue = true;
			public static boolean getIsContainerClosedTooDefaultValue() {
				return IsContainerClosedTooDefaultValue;
			}
		private boolean isContainerClosedToo = EjbContextCloseTask.getIsContainerClosedTooDefaultValue();
			/**
			 * Indicates whether EJB container is also closed automatically after closing EJB context.<br />
			 * Default to {@value #IsContainerClosedTooDefaultValue}.
			 * 
			 * @return true to close EJB container as well as EJB context, otherwise false.
			 */
			public boolean isContainerClosedToo() {
				return isContainerClosedToo;
			}
			/**
			 * Set whether EJB container is also closed automatically after closing EJB context.<br />
			 * 
			 * @param isContainerClosedToo : true to close EJB container as well as EJB context, 
			 * otherwise false.
			 */
			public void setContainerClosedToo( boolean isContainerClosedToo) {
				this.isContainerClosedToo = isContainerClosedToo;
			}
			
		public EjbContextCloseTask( 
				final String ejbModuleName,
				final EJBContainer ejbContainer, 
				final Context context, 
				final ReentrantLock lockForClose, 
				final long closeLockAcquireTimeOut) {
			setEjbModuleName( ejbModuleName);
			setEjbContainer( ejbContainer);
			setContext( context);
			this.lockForClose = lockForClose; 
			setCloseLockAcquireTimeOut( closeLockAcquireTimeOut);
		}
		
		private boolean hasBeenClosed = false;
			/**
			 * Indicates whether EJB context available via the <code>{@link #getContext()}</code> method  
			 * has been already closed.
			 * 
			 * @return true when EJB context has already been closed before, otherwise false.
			 */
			public boolean hasBeenClosed() {
				return hasBeenClosed;
			}
			/**
			 * Set value for indicating whether EJB context available via the 
			 * <code>{@link #getContext()}</code> method has been already closed.
			 * 
			 * @param hasBeenClosed : true when EJB context has already been closed before, 
			 * otherwise false.
			 */
			protected void setHasBeenClosed( boolean hasBeenClosed) {
				this.hasBeenClosed = hasBeenClosed;
			}
		private boolean isCanceled = false;
			/**
			 * Indicates whether this task has been canceled.<br />
			 * 
			 * @return true when this task has been canceled, otherwise false.
			 */
			public boolean isCanceled() {
				return isCanceled;
			}
			/**
			 * Set value for indicating whether this task has been canceled.<br />
			 * 
			 * @param isCanceled : true when this task has been canceled, otherwise false.
			 */
			protected void setCanceled( boolean isCanceled) {
				this.isCanceled = isCanceled;
			}
		private boolean hasScheduled = false;
			/**
			 * Indicates whether this task has been scheduled.<br />
			 * 
			 * @return true when this task has been scheduled, otherwise false. 
			 */
			public boolean hasScheduled() {
				return hasScheduled;
			}
			/**
			 * Set value for indicating whether this task has been scheduled.<br />
			 * 
			 * @param hasScheduled : true when this task has been scheduled, otherwise false.
			 */
			protected void setHasScheduled( boolean hasScheduled) {
				this.hasScheduled = hasScheduled;
			}
		/**
		 * {@inheritDoc}<br />
		 * Close EJB context (and container when <code>{@link #isContainerClosedToo()}</code> method 
		 * returns true) at scheduled time, and set indicator for EJB context to have been closed.<br />
		 * 
		 * @see #hasBeenClosed()
		 */
		@Override
		public void run() {
			final Logger logger = getLogger();
			
			final long closeLockAcquireTimeOut = getCloseLockAcquireTimeOut();
			final ReentrantLock lockForClose = getLockForClose();
			boolean isLocked = false;
				try {
					isLocked 
					= lockForClose.tryLock( closeLockAcquireTimeOut, TimeUnit.MILLISECONDS);
				} 
				catch ( InterruptedException exception) {
					Thread.currentThread().interrupt();
				}
			if ( isLocked) {
				try {
					if ( isCanceled()) return;
					if ( hasBeenClosed()) return;
					
					final Context context = getContext();
						if ( context == null) {
							throw new IllegalStateException(
									MessageFormat.format( 
											"Failure in closing EJB context: " 
											+ "{0,choice,0#EJB module name and context have|" 
											+ "0<for {1} EJB module, context has} not been set.",
											((getEjbModuleName() == null) ? 0 : 1),
											getEjbModuleName())
									);
						}
					try {
						context.close();
					} 
					catch ( NamingException exception) { 
						if ( logger.isDebugEnabled()) {
							logger.debug( 
									String.format(
											"Failure in closing EJB context for %1$s EJB module.",
											getEjbModuleName()), 
									exception);
						}
					}
					
					final EJBContainer ejbContainer = getEjbContainer();
					if ( isContainerClosedToo()) {
						if ( ejbContainer != null) {
							getEjbContainer().close();
						}
						else {
							if ( logger.isDebugEnabled()) {
								logger.debug(
										String.format(
												"")
										);
							}
						}
					}
					
					setHasBeenClosed( true);
						if ( logger.isInfoEnabled()) {
							logger.info( 
									MessageFormat.format(
											"Ended close opration of EJB context for {0}" 
											+ "{1,choice,0# and EJB container.|0<.}",
											getEjbModuleName(), 
											(isContainerClosedToo() && (ejbContainer != null) ? 0 : 1))
									);
						}
				}
				finally {
					lockForClose.unlock();
				}
			}
			else {
				if ( logger.isWarnEnabled()) {
					logger.warn( 
							String.format(
									"Failed to close EJB context for %1$s EJB module because lock for " 
									+ "that operation could not be acquired within %2$d [ms].",
									getEjbModuleName(), 
									closeLockAcquireTimeOut)
							);
				}
			}
		}
	}
	private EjbContextCloseTask closeTask;
		/**
		 * Getter of <code>{@link EjbContextCloseTask}</code> instance being used to close EJB context.<br /> 
		 * 
		 * @return <code>{@link EjbContextCloseTask}</code> instance being used to close EJB context.
		 * @see #close()
		 */
		protected EjbContextCloseTask getCloseTask() {
			return closeTask;
		}
		/**
		 * Setter of <code>{@link EjbContextCloseTask}</code> instance being used to close EJB context.<br />
		 * 
		 * @param closeTask : <code>{@link EjbContextCloseTask}</code> instance being used to close EJB 
		 * context.
		 * @see #close()
		 */
		protected void setCloseTask( final EjbContextCloseTask closeTask) {
			this.closeTask = closeTask;
		}
		
	/**
	 * {@inheritDoc}
	 */
	@Override
	public String getEjbModuleName() {
		return getCloseTask().getEjbModuleName();
	}
//		@Override
	public void setEjbModuleName( final String ejbModuleName) {
		getCloseTask().setEjbModuleName( ejbModuleName);
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public EJBContainer getEjbContainer() {
		return getCloseTask().getEjbContainer();
	}
//		@Override
	public void setEjbContainer( final EJBContainer ejbContainer) {
		getCloseTask().setEjbContainer( ejbContainer);
	}
	
	/**
	 * {@inheritDoc}
	 */
	@Override
	public Context getContext() {
		return getCloseTask().getContext();
	}
//		@Override
	public void setContext( final Context context) {
		getCloseTask().setContext( context);
	}
	
	public EjbContextCloseHandlerImpl( final EjbContainerMate ejbContainerMate) {
		final EjbContextCloseTask closeTask 
		= new EjbContextCloseTask( 
				ejbContainerMate.getEjbModuleName(), 
				ejbContainerMate.getEjbContainer(), 
				ejbContainerMate.getContext(),
				getLockForClose(), 
				getCloseLockAcquireTimeOut());
		setCloseTask( closeTask);
	}
	
	private Timer timer = new Timer();
		/**
		 * Getter of Timer object being used to schedule EJB context close task.<br /> 
		 * 
		 * @return Timer object being used to schedule EJB context close task.
		 * @see #close()
		 */
		protected Timer getTimer() {
			return timer;
		}
	/**
	 * {@inheritDoc}<br />
	 */
	@Override
	public void cancelClose() {
		final EjbContextCloseHandlerImpl.EjbContextCloseTask closeTask = getCloseTask();
		if ( closeTask.hasBeenClosed()) {
			final Logger logger = getLogger();
			if ( logger.isDebugEnabled()) {
				logger.debug(
						String.format(
								"Quited cancel operation of closing EJB context for %1$s " 
								+ "EJB module because it had been already closed before.",
								getEjbModuleName())
						);
			}
		}
		else {
			closeTask.cancel();
			closeTask.setCanceled( true);
			
			final EjbContextCloseTask ejbContextCloseTask
			= new EjbContextCloseTask( 
					closeTask.getEjbModuleName(),
					closeTask.getEjbContainer(), 
					closeTask.getContext(), 
					closeTask.getLockForClose(),
					closeTask.getCloseLockAcquireTimeOut());
				ejbContextCloseTask.setContainerClosedToo( closeTask.isContainerClosedToo());
			setCloseTask( ejbContextCloseTask);
		}
	}
	
	/**
	 * {@inheritDoc}<br />
	 * @see #getCloseTask()
	 * @see #getCloseDelay()
	 */
	@Override
	public void close() {
		final Logger logger = getLogger();
		
		final EjbContextCloseHandlerImpl.EjbContextCloseTask closeTask = getCloseTask();
		if ( closeTask.hasBeenClosed()) {
			if ( logger.isDebugEnabled()) {
				logger.debug(
						String.format(
								"Skipped close operation on EJB context for %1$s EJB module " 
								+ "because it had been already closed before.",
								getEjbModuleName())
						);
			}
		}
		else {
			if ( closeTask.hasScheduled()) {
				if ( logger.isDebugEnabled()) {
					logger.debug(
							String.format(
									"Skipped scheduling close operation on EJB context for %1$s " 
									+ "EJB module because it had been already scheduled to " 
									+ "be run at %2$s.",
									getEjbModuleName(), 
									(new Date( closeTask.scheduledExecutionTime())))
							);
				}
			}
			else {
				closeTask.setHasScheduled( true);
				getTimer().schedule( closeTask, getCloseDelay());
					if ( logger.isDebugEnabled()) {
						logger.debug(
								String.format(
										"Close operation on EJB context for %1$s EJB module " 
										+ "is scheduled to be run at %2$s.",
										getEjbModuleName(), 
										(new Date( closeTask.scheduledExecutionTime())))
								);
					}
			}
		}
	}
	
	/**
	 * {@inheritDoc}
	 */
	@Override
	public boolean hasBeenClosed() {
		return getCloseTask().hasBeenClosed();
	}
}